出处:掘金
原作者:ErpanOmer
我们每天的开发,可能都是从一个 npm run dev 开始的。npm scripts 对我们来说,天天用它,但很少去思考它
不信,你看看你项目里的 package.json,是不是长这样:
"scripts": {
"dev": "vite",
"build": "rm -rf dist && tsc && vite build", // 嘿,眼熟吗?
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "vitest",
"test:watch": "vitest --watch",
"preview": "vite preview"
}
这能用吗?当然能用
但这专业吗?在我看来,未必!
一个好的 scripts,应该是原子化的、跨平台的。而上面这个,一个 build 命令就不行,而且 rm -rf 在 Windows 上还得装特定环境才能跑
今天,我就来聊聊,如何用 pre、post 和 --,把你的脚本,升级成专业的脚本
pre 和 post:命令的生命周期钩子pre 和 post,是 npm 内置的一种钩子机制
它的规则很简单:
npm run xyz 时,npm 会自动先去找,有没有一个叫 prexyz 的脚本,有就先执行它xyz 执行成功后,npm 会自动再去找,有没有一个叫 postxyz 的脚本,有就最后再执行它这个自动的特性,就是神一般的存在
我们来改造那个前面👆提到的 build 脚本
&& 手动编排"scripts": {
"clean": "rimraf dist", // rimraf 解决跨平台删除问题
"lint": "eslint .",
"build:tsc": "tsc",
"build:vite": "vite build",
"build": "npm run clean && npm run lint && npm run build:tsc && npm run build:vite"
}
你的 build 脚本,它必须记住所有的前置步骤。如果哪天你想在 build 前,再加一个 test,还得去修改 build 的定义。这违反了单一职责
pre 自动触发"scripts": {
"clean": "rimraf dist",
"lint": "eslint .",
"test": "vitest run",
"build:tsc": "tsc",
"build:vite": "vite build",
// build的前置钩子
"prebuild": "npm run clean && npm run lint && npm run test",
// build的核心命令
"build": "npm run build:tsc && npm run build:vite",
// build的后置钩子
"postbuild": "echo 'Build complete! Check /dist folder.'"
}
看到区别了吗?
现在,当我只想构建时,我依然执行 npm run build
npm 会自动帮我执行 prebuild(清理、Lint、测试)→ 然后执行 build(编译、打包)→ 最后执行 postbuild(打印日志)
我的 build 脚本,只关心构建这件事。而 prebuild 脚本,只关心前置检查这件事
这就是单一职责和关注点分离
你甚至可以利用这个特性,搞点骚操作😁:
"scripts": {
// 当你执行 npm start 时,它会自动先执行 npm run build
"prestart": "npm run build",
"start": "node dist/server.js"
}
-- 双短线:脚本参数-- 是我最爱的一个特性。它是一个参数分隔符
它的作用是:告诉 npm,我的 npm 参数到此为止了,后面所有的东西,都原封不动地,传给我要执行的那个底层命令
我们来看开头👆那个脚本:
"scripts": {
"test": "vitest",
"test:watch": "vitest --watch"
}
为了一个 --watch 参数,你复制了一个几乎一模一样的脚本。如果明天你还想要 --coverage 呢?再加一个 test:coverage?这叫垃圾代码💩
专业写法:用 -- 动态传参
"scripts": {
"test": "vitest"
}
就这一行,够了
等等,那我怎么跑 watch 和 coverage?
答案,就是用 --:
# 1. 只跑一次
$ npm run test -- --run
# 实际执行: vitest --run
# 2. 跑 watch 模式
$ npm run test -- --watch
# 实际执行: vitest --watch
# 3. 跑覆盖率
$ npm run test -- --coverage
# 实际执行: vitest --coverage
# 4. 跑某个特定文件
$ npm run test -- src/my-component.test.ts
# 实际执行: vitest src/my-component.test.ts
-- 就像一个参数隧道 ,它把你在命令行里,跟在 -- 后面的所有参数,原封不动地扔给了 vitest 命令
好了,我们把 pre / post 和 -- 结合起来,看看一个专业的 package.json 是长什么样子:
"scripts": {
// 1. Lint
"lint": "eslint .",
"lint:fix": "eslint . --fix",
// 2. Test
"test": "vitest",
"pretest": "npm run lint", // 在test前,必须先lint
// 3. Build
"build": "tsc && vite build",
"prebuild": "npm run test -- --run", // 在build前,必须先test通过
// 4. Publish (发布的前置钩子)
// prepublishOnly 是一个 npm 内置的、比 prepublish 更安全的钩子
// 它只在 npm publish 时执行,而在 npm install 时不执行
"prepublishOnly": "npm run build" // 在发布前,必须先build
}
看看我们构建了怎样一条自动化脚本:
npm publish,准备发布npm 一看,有个 prepublishOnly,于是它先去执行 npm run buildnpm 一看,build 有个 prebuild,于是它又先去执行 npm run test -- --runnpm 一看,test 有个 pretest,于是它又双叒叕先去执行 npm run lint最终的执行流是:Lint → Test → Build → Publish
这些脚本,被 pre 钩子,自动地、强制地串联了起来。你作为开发者,根本没有机会犯错。你不可能发布一个连 Lint 都没过或者测试未通过的包
npm scripts,它不是一个简单的脚本快捷方式。它是一个工作流(Workflow)的定义
pre 和 post,定义了你工作流的执行顺序和依赖,保证了代码检查等功能,而 -- 是确保你工作流中的脚本参数